%%%-----------------------------------------------------------------------------
%%% @doc
%%% apn_server recompiler.
%%%
%%% @end
%%%-----------------------------------------------------------------------------
-module(apn_server_recompiler).

-include_lib("kernel/include/file.hrl").

-behaviour(gen_server).
-export([start/0, start_link/0]).
-export([stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([compile/1, compile/2]).
-record(state, {last, tref}).

-define(TIMER, 2000).

%% External API

%% @spec start() -> ServerRet
%% @doc Start the reloader.
start() ->
    gen_server:start({local, ?MODULE}, ?MODULE, [], []).

%% @spec start_link() -> ServerRet
%% @doc Start the reloader.
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% @spec stop() -> ok
%% @doc Stop the reloader.
stop() ->
    gen_server:call(?MODULE, stop).

%% gen_server callbacks

%% @spec init([]) -> {ok, State}
%% @doc gen_server init, opens the server in an initial state.
init([]) ->
    % {ok, TRef} = timer:send_interval(timer:seconds(1), doit),
    % {ok, #state{last = stamp(), tref = TRef}}.
    erlang:send_after(?TIMER, self(), doit),
    {ok, #state{last = stamp()}}.
 
%% @spec handle_call(Args, From, State) -> tuple()
%% @doc gen_server callback.
handle_call(stop, _From, State) ->
    {stop, shutdown, stopped, State};
handle_call(_Req, _From, State) ->
    {reply, {error, badrequest}, State}.

%% @spec handle_cast(Cast, State) -> tuple()
%% @doc gen_server callback.
handle_cast(_Req, State) ->
    {noreply, State}.

%% @spec handle_info(Info, State) -> tuple()
%% @doc gen_server callback.
handle_info(doit, State) ->
    Now = stamp(),
    doit(State#state.last, Now),
    erlang:send_after(?TIMER, self(), doit),
    {noreply, State#state{last = Now}};
handle_info(_Info, State) ->
    {noreply, State}.

%% @spec terminate(Reason, State) -> ok
%% @doc gen_server termination callback.
terminate(_Reason, _State) ->
    % {ok, cancel} = timer:cancel(State#state.tref),
    ok.


%% @spec code_change(_OldVsn, State, _Extra) -> State
%% @doc gen_server code_change callback (trivial).
code_change(_Vsn, State, _Extra) ->
    {ok, State}.

to_atom(Atom) when is_list(Atom)  -> 
    try
        erlang:list_to_existing_atom(Atom)
    catch
        _:_ -> 
            erlang:list_to_atom(Atom)
    end;
to_atom(Atom) -> 
    Atom.

% "/usr/dir/filename.ext"  ".ext"
filename_to_modulename(Filename, Ext) ->
    Basename = filename:basename(Filename),
    case string:right(Basename, string:len(Ext)) == Ext of 
        true ->
            to_atom(string:left(Basename, string:len(Basename)-string:len(Ext)));
        _ ->
            unknowned
    end.

get_includes() ->
    lists:foldl(
        fun(X, {R1, R2, R3}) -> 
            {Includedir, Srcdir} = case string:right(X, 5) == "/ebin" of 
                        true -> 
                            {[{i, filename:dirname(X) ++ "/include"}], filename:dirname(X) ++ "/src"};
                        _ ->
                            {[], X} 
                    end,

            {Includedir ++ R1, 
            [Srcdir|R2], 
            [X|R3]}
        end, {[], [], []}, code:get_path()).

doit(From, To) ->
    {Includes, Srcdirs, _EBindirs} = get_includes(),
    % io:format("doit(~p)~n", [{Includes, Srcdirs, EBindirs}]),
    lists:map(
        fun(X) ->
             filelib:fold_files(X, ".*.erl", false, 
                fun(F, _) -> 
                    {_ret} = try_compile(filename_to_modulename(F, ".erl"), F, From, To, Includes),
                    ok
                end, [])
        end, Srcdirs),
    % io:format("ReloadedBeams(~p)~n", [ReloadedBeams]),

    ok.

compile(Filename) ->
    {Includes, Srcdirs, _EBindirs} = get_includes(),
    % check is full name
    Filename1 = 
        case length(filename:dirname(Filename)) > 1 of 
            true ->
                Filename;
            _ ->
                % find full name in srcdirs
                lists:foldl(
                    fun(X, FName) ->
                         filelib:fold_files(X, ".*.erl", false, 
                            fun(F, FName1) -> 
                                case string:str(F, Filename) > 0 of 
                                    true ->
                                        F;
                                    _ ->
                                        FName1
                                end
                            end, FName)
                    end, Filename, Srcdirs)
        end,
    erlang:spawn(?MODULE, compile, [Filename1, Includes]).

compile(Filename, Includes) ->
    Beamfilename = 
        case string:right(filename:dirname(Filename), 4) == "/src" of 
            true -> 
                filename:dirname(filename:dirname(Filename)) ++ "/ebin/" ++ string:left(filename:basename(Filename), string:len(filename:basename(Filename))-3) ++ "beam";
            _ ->
                filename:dirname(Filename) ++ "/" ++ string:left(filename:basename(Filename), string:len(filename:basename(Filename))-3) ++ "beam"
        end,
    % io:format("try_compile(~p)~n", [{Filename, Beamfilename}]),
    io:format("try_compile(~p)~n", [{Filename}]),  
    CompRet = compile:file(Filename, [verbose,report_errors,report_warnings, {outdir,filename:dirname(Beamfilename)++"/"}, {parse_transform, lager_transform}] ++ Includes),
    {Beamfilename, CompRet}.

try_compile(Module, Filename, From, To, Includes) ->
    % io:format("try_compile(~p)~n", [Filename]),
    case file:read_file_info(Filename) of
         {ok, #file_info{mtime = Mtime}} when Mtime >= From andalso Mtime < To ->
            {_Beamfilename, CompRet} = compile(Filename, Includes),
             % io:format("now(~w)~n", [{Now, To}]),  
             apn_server_reloader:reload(Module),

             {CompRet};
         {ok, _} ->
             {unmodified};
         {error, enoent} ->
             %% The Erlang compiler deletes existing .beam files if
             %% recompiling fails.  Maybe it's worth spitting out a
             %% warning here, but I'd want to limit it to just once.
             {gone};
         {error, enotdir} ->  
            {gone};
         {error, Reason} ->
             io:format("Error reading ~s's file info: ~p~n",
                       [Filename, Reason]),
             {error}
     end.

stamp() ->
    erlang:localtime().

%%
%% Tests
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
